package com.antonytrupe.gamething;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.jsr107cache.Cache;
import net.sf.jsr107cache.CacheException;
import net.sf.jsr107cache.CacheManager;

import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.Scriptable;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Text;

public class GameEngine {

	public final class IO {
		private static final String ioWrappers = "var engine = engine||new Object();\n"
				+ "engine.io=engine.io||new Object;\n"
				+ "engine.io.write=function(string){e.io.write(string);};";

		public void write(String string) {
			if (string == null) {
				return;
			}
			if (GameEngine.this.response != null) {
				try {
					GameEngine.this.response.getWriter().write(string);
				} catch (IOException e) {
					e.printStackTrace();
				}
			} else {
				System.out.print(string);
			}
		}
	}

	public final class Persistence {

		private static final String persistenceWrappers = "var engine = engine||new Object();\n"
				+ "engine.persistence=engine.persistence||new Object;\n"
				+ "engine.persistence.get=function(map){\n"
				+ "return e.persistence.get(map.get('clazz')[0],map.get('name')[0]);};\n"

				+ "engine.persistence.getAll=function(map){\n"
				+ "return e.persistence.getAll(map.get('clazz')[0]);};\n"

				+ "engine.persistence.save=function(map){\n"
				+ "_map=new java.util.HashMap();\n"
				+ "var entrySetArray=map.entrySet().toArray();\n"
				+ "for (entry in entrySetArray) {\n"
				// invoke name clazz
				+ "if(entrySetArray[entry].getKey()!='invoke' && entrySetArray[entry].getKey()!='name' && entrySetArray[entry].getKey()!='clazz'){\n" //$NON-NLS-1$
				+ "_map.put(entrySetArray[entry].getKey(),entrySetArray[entry].getValue()[0]);\n"
				+ "}\n"
				+ "}\n"
				+ "e.persistence.save(map.get('clazz')[0],map.get('name')[0],_map);\n"
				+ "};";

		@SuppressWarnings("unchecked")
		public HashMap<String, String> get(String clazz, String name) {
			Cache cache;
			HashMap<String, String> object;
			try {
				cache = CacheManager.getInstance().getCacheFactory()
						.createCache(Collections.emptyMap());
				// Get the value from the cache.
				object = (HashMap<String, String>) cache.get(name);
				if (object != null) {
					return object;
				}
			} catch (CacheException e) {
				// TODO exception handling ...
			}

			DatastoreService datastore = DatastoreServiceFactory
					.getDatastoreService();
			object = new HashMap<String, String>();
			try {
				Entity e = datastore.get(KeyFactory.createKey(clazz, name));
				for (Entry<String, Object> entry : e.getProperties().entrySet()) {
					object.put(entry.getKey(),
							((Text) entry.getValue()).getValue());
				}
				object.put("name", e.getKey().getName()); //$NON-NLS-1$

				try {
					cache = CacheManager.getInstance().getCacheFactory()
							.createCache(Collections.emptyMap());
					cache.put(name, object);
				} catch (CacheException ce) {
					// exception handling
				}
			} catch (EntityNotFoundException enf) {
				// TODO exception handling
			}
			return object;

		}

		public ArrayList<HashMap<String, String>> getAll(String clazz) {
			// TODO check memcache first
			DatastoreService datastore = DatastoreServiceFactory
					.getDatastoreService();
			ArrayList<HashMap<String, String>> objects = new ArrayList<HashMap<String, String>>();
			// The Query interface assembles a query
			Query q = new Query(clazz);

			// PreparedQuery contains the methods for fetching query results
			// from the datastore
			PreparedQuery pq = datastore.prepare(q);

			for (Entity result : pq.asIterable()) {
				// iterate over each property
				HashMap<String, String> obj = new HashMap<String, String>();
				obj.put("name", result.getKey().getName()); //$NON-NLS-1$
				for (Entry<String, Object> entry : result.getProperties()
						.entrySet()) {
					obj.put(entry.getKey(),
							((Text) entry.getValue()).getValue());
				}

				objects.add(obj);
			}
			// TODO if we had to go to the datastore, stash it in memcache for
			// the next time
			return objects;

		}

		public ArrayList<HashMap<String, String>> getAllKinds() {
			DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
			Query q = new Query("__Stat_Kind__");
			ArrayList<HashMap<String, String>> objects = new ArrayList<HashMap<String, String>>();
			for (Entity result : ds.prepare(q).asIterable()) {

				// iterate over each property
				HashMap<String, String> obj = new HashMap<String, String>();
				obj.put("name", result.getKey().getName());
				for (Entry<String, Object> entry : result.getProperties()
						.entrySet()) {
					obj.put(entry.getKey(),
							((Text) entry.getValue()).getValue());
				}

				objects.add(obj);
			}
			// TODO if we had to go to the datastore, stash it in memcache for
			// the next time
			return objects;
		}

		public Entity save(String clazz, String name,
				Map<String, String> properties) {
			DatastoreService datastore = DatastoreServiceFactory
					.getDatastoreService();
			Entity e = new Entity(KeyFactory.createKey(clazz, name));
			{

				// set properties
				for (Entry<String, String> entry : properties.entrySet()) {
					e.setProperty(entry.getKey(), new Text(entry.getValue()));
				}

				datastore.put(e);
			}

			try {
				Cache cache = CacheManager.getInstance().getCacheFactory()
						.createCache(Collections.emptyMap());
				cache.put(name, properties);
			} catch (CacheException ce) {
				// ...
			}
			return e;
		}
	}

	static final String SCRIPT = "Script";
	static final String CODE = "code";

	public final IO io = new IO();

	public final Persistence persistence = new Persistence();

	static final String INVOKE = "invoke";
	static final String INCLUDE = "include";

	private final Context cx;
	private final Scriptable scope;
	// TODO final
	private HttpServletResponse response;

	// TODO final
	private Map<String, String[]> map;

	private String[] includes;
	private String[] invokes;

	protected GameEngine() {
		this.response = null;
		this.map = null;

		this.cx = Context.enter();
		this.scope = this.cx.initStandardObjects();

		// engine.eval(IO.ioWrappers);
		this.cx.evaluateString(this.scope, IO.ioWrappers,
				"IO.ioWrappers", 1, null); //$NON-NLS-1$
		// engine.eval(Persistence.persistenceWrappers);
		this.cx.evaluateString(this.scope, Persistence.persistenceWrappers,
				"Persistence.persistenceWrappers", 1, null); //$NON-NLS-1$

		// engine.put("e", this);
		this.scope.put("e", this.scope, Context.javaToJS(this, this.scope)); //$NON-NLS-1$
	}

	@SuppressWarnings("unchecked")
	protected GameEngine(final HttpServletRequest req, HttpServletResponse resp) {
		this(req.getParameterMap());
		// this.request = request;
		this.response = resp;

		String requestURI = req.getRequestURI();
		requestURI = requestURI.replaceFirst("/", ""); //$NON-NLS-1$ //$NON-NLS-2$
		String[] paths = requestURI.split("/"); //$NON-NLS-1$

		if (paths != null && paths.length >= 2) {
			this.includes = paths[0].split(","); //$NON-NLS-1$
			this.invokes = paths[1].split(","); //$NON-NLS-1$
		}
	}

	protected GameEngine(final Map<String, String[]> m) {
		this();
		this.map = m;
		// engine.put("parameters", map);
		this.scope
				.put("parameters", this.scope, Context.javaToJS(this.map, this.scope)); //$NON-NLS-1$
	}

	private void include(String[] incl) {
		for (String name : incl) {
			for (String n : name.split(",")) { //$NON-NLS-1$

				HashMap<String, String> script = this.persistence
						.get(SCRIPT, n);
				if (script.containsKey(CODE) && script.get(CODE) != null //$NON-NLS-1$ //$NON-NLS-2$
						&& script.get(CODE) != "") { //$NON-NLS-1$ //$NON-NLS-2$
					// engine.eval(script.get("code"));
					try {
						this.cx.evaluateString(this.scope, script.get(CODE), n,
								1, null); //$NON-NLS-1$
					} catch (RhinoException ee) {
						ee.printStackTrace();
						throw ee;
					}
				}
			}
		}
	}

	private Object invoke(final String[] inv, final Map<String, String[]> m) {
		Object result = null;
		for (String name : inv) {
			for (String n : name.split(",")) {
				try {
					// TODO handle not calling a function
					// var bob=new Cow('Bob');
					// engine.persistance.save {clazz:Cow, name:Cow}
					Object fct = this.cx.evaluateString(this.scope, n, n, 1,
							null);
					result = ((Callable) fct).call(this.cx, this.scope,
							this.scope, new Object[] { m });
				} catch (EcmaError ee) {

					throw new RuntimeException(ee);
				}
			}
		}
		return result;
	}

	protected Object process() {
		return this.process(this.map);
	}

	/**
	 * @param m
	 *            A map of values, which will typically include one or more
	 *            "invoke" keys and one or more "include" keys. The values of
	 *            invoke and include can be comma separated, and are processed
	 *            in the order they appear in the map, so use a stable
	 *            implementation.
	 * @return the result of the last invoke value
	 */
	protected Object process(final Map<String, String[]> m) {

		if (this.includes != null) {
			include(this.includes);

		}
		if (this.invokes != null) {
			invoke(this.invokes, m);
		}

		// get the scripts to load
		if (m.containsKey(INCLUDE)) {
			this.includes = m.get(INCLUDE);
			include(this.includes);
		}

		Object result = null;
		// now execute some code
		if (m.containsKey(INVOKE)) {

			this.invokes = m.get(INVOKE);
			result = invoke(this.invokes, m);
		}
		if (result != null && result instanceof NativeJavaObject) {
			return ((NativeJavaObject) result).unwrap();
		}
		return result;
	}
}